Skip to content

feature/basis for filtering cards by distance without external maps#8

Open
akshaygajjala1 wants to merge 2 commits into
mainfrom
distance-filter-and-location-setup
Open

feature/basis for filtering cards by distance without external maps#8
akshaygajjala1 wants to merge 2 commits into
mainfrom
distance-filter-and-location-setup

Conversation

@akshaygajjala1

Copy link
Copy Markdown
Collaborator

No description provided.

@pi2chen pi2chen changed the title basis for filtering cards by distance without external maps feature/basis for filtering cards by distance without external maps Mar 23, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an initial end-to-end path for distance-based study-spot ordering by introducing user location capture in the Expo app and a backend API endpoint that sorts spots by haversine distance.

Changes:

  • Add latitude/longitude fields to the StudySpot model and expose them via to_dict().
  • Add a /api/study_spots/sort_by_distance endpoint that returns spots sorted by computed distance.
  • Update the frontend to request location permissions and call the new endpoint; add expo-location and adjust TS JSX settings.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
instance/longhorn_studies.db Adds a SQLite DB artifact containing a study_spots schema snapshot.
frontend/tsconfig.json Sets jsx to react-jsx.
frontend/package.json Adds expo-location dependency.
frontend/app/(tabs)/index.tsx Requests user location and posts coordinates to the backend to fetch distance-sorted spots.
backend/routes.py Introduces /study_spots/sort_by_distance and haversine distance helper.
backend/models.py Adds latitude/longitude columns and includes them in API serialization.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/routes.py
Comment on lines +267 to +285
# Get all study spots
spots = StudySpot.query.all()

# Calculate distances and sort
spots_with_distance = []
for spot in spots:
# Skip spots without coordinates
if not hasattr(spot, 'latitude') or not hasattr(spot, 'longitude'):
continue
if spot.latitude is None or spot.longitude is None:
continue

distance = _haversine_distance(user_lat, user_lng, spot.latitude, spot.longitude)
spot_dict = spot.to_dict()
spot_dict['distance_from_user'] = distance
spots_with_distance.append(spot_dict)

# Sort by distance (closest first)
spots_with_distance.sort(key=lambda x: x['distance_from_user'])

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StudySpot.query.all() loads every row into memory and then sorts in Python. This won’t scale and can become a latency/memory issue as the table grows. Consider filtering out null coordinates at the DB level, selecting only needed columns, and optionally supporting paging/limits.

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +56
useEffect(() => {
if (userLat && userLng) {
fetchSortedByDistance();
}

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same falsy-check issue here: if (userLat && userLng) will skip when either coordinate is 0. Prefer explicit null/undefined checks before calling fetchSortedByDistance().

Copilot uses AI. Check for mistakes.

const [userLat, setUserLat] = useState<number | null>(null);
const [userLng, setUserLng] = useState<number | null>(null);
const [spots, setSpots] = useState<any[]>([]);

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spots state is set but never read/rendered in this component, which is likely to trigger no-unused-vars lint noise and makes it harder to see intended behavior. Either render/use spots (even temporarily) or remove the state until it’s needed.

Copilot uses AI. Check for mistakes.
Comment thread backend/routes.py
Comment on lines +273 to +285
# Skip spots without coordinates
if not hasattr(spot, 'latitude') or not hasattr(spot, 'longitude'):
continue
if spot.latitude is None or spot.longitude is None:
continue

distance = _haversine_distance(user_lat, user_lng, spot.latitude, spot.longitude)
spot_dict = spot.to_dict()
spot_dict['distance_from_user'] = distance
spots_with_distance.append(spot_dict)

# Sort by distance (closest first)
spots_with_distance.sort(key=lambda x: x['distance_from_user'])

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint relies on StudySpot.latitude/longitude, but the create/update JSON handler (_study_spot_from_json) doesn’t accept/populate those fields. As a result, newly created/updated spots will have null coordinates and will be filtered out here, often returning an empty list. Add latitude/longitude handling (with validation) to the create/update flow, or adjust the endpoint’s behavior.

Suggested change
# Skip spots without coordinates
if not hasattr(spot, 'latitude') or not hasattr(spot, 'longitude'):
continue
if spot.latitude is None or spot.longitude is None:
continue
distance = _haversine_distance(user_lat, user_lng, spot.latitude, spot.longitude)
spot_dict = spot.to_dict()
spot_dict['distance_from_user'] = distance
spots_with_distance.append(spot_dict)
# Sort by distance (closest first)
spots_with_distance.sort(key=lambda x: x['distance_from_user'])
spot_dict = spot.to_dict()
# If the spot doesn't have usable coordinates, include it with no distance
if (not hasattr(spot, 'latitude') or not hasattr(spot, 'longitude') or
spot.latitude is None or spot.longitude is None):
spot_dict['distance_from_user'] = None
spots_with_distance.append(spot_dict)
continue
# Compute distance for spots with valid coordinates
distance = _haversine_distance(user_lat, user_lng, spot.latitude, spot.longitude)
spot_dict['distance_from_user'] = distance
spots_with_distance.append(spot_dict)
# Sort by distance (closest first); spots without distance go last
spots_with_distance.sort(
key=lambda x: (
x['distance_from_user'] is None,
x['distance_from_user'] if x['distance_from_user'] is not None else float('inf'),
)
)

Copilot uses AI. Check for mistakes.
Comment thread backend/routes.py
user_lat = float(user_lat)
user_lng = float(user_lng)
except (ValueError, TypeError):
return jsonify({'error': 'Invalid coordinates'}), 400

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coordinate validation here only checks “is present” and “can be cast to float”. It should also validate bounds (lat ∈ [-90, 90], lng ∈ [-180, 180]) to avoid nonsensical values and surprising distance calculations.

Suggested change
return jsonify({'error': 'Invalid coordinates'}), 400
return jsonify({'error': 'Invalid coordinates'}), 400
# Validate coordinate bounds
if not (-90 <= user_lat <= 90) or not (-180 <= user_lng <= 180):
return jsonify({'error': 'Coordinates out of range'}), 400

Copilot uses AI. Check for mistakes.
}

async function fetchSortedByDistance() {
if (!userLat || !userLng) return;

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!userLat || !userLng) return; treats 0 as “missing”, which is incorrect for valid coordinates. Use explicit null/undefined checks (e.g., userLat == null || userLng == null) to decide whether coordinates are available.

Suggested change
if (!userLat || !userLng) return;
if (userLat == null || userLng == null) return;

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +34
const response = await fetch('http://localhost:8000/api/study_spots/sort_by_distance', {
method: 'POST',

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coding http://localhost:8000 will fail on physical devices and many emulator/simulator setups because localhost resolves to the device itself. Consider centralizing the API base URL (env/config) and using platform-appropriate host resolution (e.g., dev machine LAN IP / Android emulator 10.0.2.2).

Copilot uses AI. Check for mistakes.
user_lat: userLat,
user_lng: userLng
})
});

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchSortedByDistance doesn’t check response.ok before calling response.json(), so non-2xx responses (or non-JSON error bodies) can throw or silently produce confusing UI state. Handle non-OK responses explicitly (and consider surfacing the API error message).

Suggested change
});
});
if (!response.ok) {
const errorText = await response.text().catch(() => '');
throw new Error(
`Request failed with status ${response.status} ${response.statusText}${
errorText ? `: ${errorText}` : ''
}`,
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +26
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
return;
}

const location = await Location.getCurrentPositionAsync({});
setUserLat(location.coords.latitude);
setUserLng(location.coords.longitude);

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Location.getCurrentPositionAsync can throw (e.g., location services disabled, timeout). Wrap this call in try/catch and handle failures (at least log and avoid leaving state in an indeterminate null/partial state).

Suggested change
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
return;
}
const location = await Location.getCurrentPositionAsync({});
setUserLat(location.coords.latitude);
setUserLng(location.coords.longitude);
try {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
console.warn('Location permission not granted');
setUserLat(null);
setUserLng(null);
return;
}
const location = await Location.getCurrentPositionAsync({});
if (!location || !location.coords) {
console.error('Failed to obtain location coordinates');
setUserLat(null);
setUserLng(null);
return;
}
setUserLat(location.coords.latitude);
setUserLng(location.coords.longitude);
} catch (error) {
console.error('Error getting user location:', error);
setUserLat(null);
setUserLng(null);
}

Copilot uses AI. Check for mistakes.
Comment thread backend/models.py
Comment on lines +20 to +21
latitude = db.Column(db.Float, nullable=True)
longitude = db.Column(db.Float, nullable=True)

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding latitude/longitude columns requires a DB schema change. Since the app currently uses db.create_all() (which won’t alter existing tables), any existing SQLite DB will be incompatible and queries can fail. Consider adding a migration step / schema update script (or ensure the dev DB is recreated) as part of this change.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants